<template>
  <div class="d3-wrapper">
    <div
      class="chart-header"
      style="
        height: 40px;
        display: flex;
        align-items: center;
        justify-content: space-between;
      "
    >
      <div class="chart-header-title" style="font-size: 18px; font-weight: 700">
        <el-icon style="cursor: pointer" @click="router.back()"
          ><ArrowLeft
        /></el-icon>
        知识图谱
      </div>
      <el-input
        v-model="query.keyword"
        style="width: 300px"
        placeholder="请输入搜索内容"
        class="input-with-select"
        clearable
        @keydown.enter="handleChangeKeyword"
      >
        <template #append>
          <el-button
            :disabled="query.keyword && !query.keyword.trim()"
            :icon="Search"
            @click="handleChangeKeyword"
          />
        </template>
      </el-input>
    </div>
    <div class="graph-container" style="display: flex" v-loading="loading">
      <div
        v-if="nodes.length"
        id="graph"
        ref="svgContainer"
        class="graph-containers"
      ></div>
      <div class="graph-containers" v-else>
        <el-empty style="margin: 0 auto"></el-empty>
      </div>
      <RightPanel></RightPanel>
    </div>

    <el-drawer v-model="drawer" :show-close="false">
      <template #header>
        <header style="font-size: 20px; font-weight: bold">
          {{ showDetail.name ? showDetail.name : '' }}
        </header>
      </template>
      <template v-if="JSON.stringify(showDetail) !== '{}'">
        <div class="desc" style="font-size: 14px; color: #aaa">
          <span style="font-size: 14px; margin-right: 10px">描述:</span>
          {{ showDetail.description ? showDetail.description : '无' }}
        </div>
      </template>
      <el-empty v-else></el-empty>
    </el-drawer>
  </div>
</template>
<script setup lang="ts">
import { ref, shallowRef, onMounted, onUnmounted, reactive } from 'vue'
import iconDo from '@/assets/icon-do.png'
import iconRe from '@/assets/icon-relationship.png'
import iconDe from '@/assets/icon-description.png'
import * as d3 from 'd3'
import { useRouter, useRoute } from 'vue-router'
import { httpKnowledgeChart, httpKeywords } from '@/api/study'
import RightPanel from './RightPanel.vue'
import { debounce } from '@/utils/tools'
import { Search } from '@element-plus/icons-vue'

const loading = ref(false)
const router = useRouter()
const route = useRoute()
const width = shallowRef()
const height = shallowRef()
const svgContainer = shallowRef(null)
const drawer = ref(false)
const showDetail = ref<any>({})
// 节点和连线数据
const nodes = ref([])
const links = ref([])
// 显示关系文案
let relationshipText = null
// const svg = shallowRef(null)
const simulation = shallowRef(null)
const selectedNode = ref<any>({})
const keyWordList = ref([])
const query = reactive({
  keyword: undefined,
})
const keywordLoading = ref(false)
const remoteMethod = debounce(async (query: string) => {
  try {
    keywordLoading.value = true
    const res: any = await httpKeywords({
      course_id: route.query.cid,
      keyword_name: query,
      limit: 6,
    })
    if (res) {
      keyWordList.value = res.keywords
    }
  } catch (err) {
    console.error(err)
  } finally {
    keywordLoading.value = false
  }
}, 1000)
function removeRelationshipText() {
  if (relationshipText) {
    relationshipText.remove() // 移除文案
  }
}

// 判断节点是否与另一个节点连接
function isConnected(node1, node2) {
  return links.value.some(
    (link) =>
      (link.source === node1 && link.target === node2) ||
      (link.source === node2 && link.target === node1),
  )
}

// 拖动事件
function dragStarted(event, d) {
  if (!event.active) simulation.value.alphaTarget(0.3).restart()
  d.fx = d.x
  d.fy = d.y
}

function dragged(event, d) {
  d.fx = event.x
  d.fy = event.y
}

function dragEnded(event, d) {
  if (!event.active) simulation.value.alphaTarget(0)
  d.fx = null
  d.fy = null
}

function showRelationshipText(linkData, g) {
  if (relationshipText) {
    relationshipText.remove() // 移除之前的关系文本
  }

  relationshipText = g
    .append('text')
    .attr('class', 'relationship-text')
    .attr('x', (linkData.source.x + linkData.target.x) / 2)
    .attr('y', (linkData.source.y + linkData.target.y) / 2)
    .attr('dy', -10) // 文本上移一点
    .text(linkData.relationship)
}

const renderChart = () => {
  // 创建SVG容器
  const svg = d3
    .select('#graph')
    .append('svg')
    .attr('width', width.value)
    .attr('height', height.value)
  const color = d3.scaleOrdinal(d3.schemeCategory10)
  const g = svg.append('g')
  // 创建力学模拟器
  simulation.value = d3
    .forceSimulation(nodes.value)
    .force(
      'link',
      d3
        .forceLink(links.value)
        .id((d) => d.id)
        .distance(50),
    )
    .force(
      'charge',
      d3
        .forceManyBody()
        .distanceMin(10)
        .distanceMax(200)
        .strength(0.5)
        .theta(0.4),
    )
    .force('center', d3.forceCenter(width.value / 2, height.value / 2))
    .force('collision', d3.forceCollide().radius(50).strength(1))
  for (let i = 0; i < 50; i++) {
    simulation.value.tick()
  }
  loading.value = false
  // 定义箭头
  g.append('defs')
    .append('marker')
    .attr('id', 'arrowhead')
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 26) // 设置箭头的偏移量
    .attr('refY', 0)
    .attr('orient', 'auto')
    .attr('markerWidth', 6)
    .attr('markerHeight', 6)
    .append('path')
    .attr('d', 'M0,-5L10,0L0,5')
    .attr('fill', '#aaa')
  // 绘制连线
  const link = g
    .append('g')
    .selectAll('.link')
    .data(links.value)
    // .enter()
    // .append('line')
    .join('line')
    .attr('class', 'link')
    .style('stroke', '#aaa')
    .attr('marker-end', 'url(#arrowhead)')
    .style('stroke-width', 2)
    .on('mouseover', function (event, d) {
      event.stopPropagation()
      // d3.select(this)
      //   .style('stroke-width', 5)
      //   .style('stroke', 'rgba(135, 206, 235, 0.3)')
      //   .style('stroke-padding', '5px')
      // .style('filter', 'drop-shadow(0 0 4px rgba(75, 0, 130, 0.5))')

      showRelationshipText(d, g)
    })
    .on('mouseout', function (event, d) {
      d3.select(this)
        .style('stroke-width', 2)
        .style('stroke-padding', '0px')
        .style('stroke', '#aaa')
        .style('filter', 'none')
      removeRelationshipText()
    })

  // 绘制节点
  const node = g
    .append('g')
    .selectAll('.node')
    .data(nodes.value)
    .join('g')
    .attr('class', 'node')
    .attr('fill', (d) => color(d.group))
    .on('mouseover', function (event, d) {
      // 添加外边环
      d3.select(this)
        .style('background-color', 'rgba(255, 255, 255, 0.3)')
        .style('cursor', 'pointer')
        .append('circle')
        .attr('class', 'hover-ring')
        .attr('r', 25) // 比内圆大一些
        .attr('fill', 'none')
        .attr('stroke', d3.select(this).attr('fill'))
        .attr('stroke-width', 3)
        .style('opacity', 0.5)
    })
    .on('mouseout', function (event, d) {
      // 移除外边环
      d3.select(this).select('.hover-ring').remove()
    })
    .call(
      d3
        .drag()
        .on('start', dragStarted)
        .on('drag', dragged)
        .on('end', dragEnded),
    )

  // 绘制小圆（内层圆）
  node.append('circle').attr('class', 'small-circle').attr('r', 20)

  // 为节点添加文案
  node
    .append('text')
    .attr('class', 'node-text')
    .attr('text-anchor', 'middle') // 水平居中
    .attr('y', 40) // 文案位置，放在节点下方
    .text((d) => d.name) // 显示节点的名称

  // 创建大圆环和扇形
  let currentBigCircle = null // 当前显示的大圆环
  let currentArcGroup = null // 当前显示的扇形

  // 更新力学模拟器
  simulation.value.on('tick', function () {
    link
      .attr('x1', (d) => d.source?.x)
      .attr('y1', (d) => d.source?.y)
      .attr('x2', (d) => d.target?.x)
      .attr('y2', (d) => d.target?.y)

    node.attr('transform', (d) => `translate(${d.x}, ${d.y})`)

    // 更新圆环和扇形的位置
    if (currentBigCircle) {
      currentBigCircle
        .attr('cx', currentBigCircle.data()[0].x)
        .attr('cy', currentBigCircle.data()[0].y)

      currentArcGroup.attr(
        'transform',
        `translate(${currentBigCircle.data()[0].x}, ${
          currentBigCircle.data()[0].y
        })`,
      )
    }
  })

  // 点击节点时，显示或隐藏大圆环和扇形
  node.on('click', function (event, d) {
    selectedNode.value = d
    event.stopPropagation() // 防止触发 svg 区域的点击事件
    // 如果当前已有显示的大圆环，且是同一节点，则移除它
    if (currentBigCircle && currentBigCircle.data()[0] === d) {
      currentBigCircle.remove()
      currentArcGroup.remove()
      currentBigCircle = null
      currentArcGroup = null
    } else {
      // 移除之前显示的圆环和扇形
      if (currentBigCircle) {
        currentBigCircle.remove()
        currentArcGroup.remove()
      }

      // 创建新的大圆环
      const radius = 60
      currentBigCircle = g
        .append('circle')
        .attr('class', 'big-circle')
        .attr('r', radius)
        .attr('cx', d.x)
        .attr('cy', d.y)
        .attr('fill', 'transparent')
        .data([d]) // 将节点数据绑定到圆环

      // 创建扇形区域（带间隔）
      const angleStep = (Math.PI * 2) / 3 // 每个扇形占120度
      const arc = d3
        .arc()
        .innerRadius(radius - 32) // 内半径
        .outerRadius(radius) // 外半径
        .startAngle((d, i) => i * angleStep + 0.05) // 扇形起始角度（增加间隔）
        .endAngle((d, i) => (i + 1) * angleStep - 0.05) // 扇形结束角度（增加间隔）
      // 创建扇形组
      currentArcGroup = g
        .append('g')
        .attr('class', 'arc-group')
        .attr('transform', `translate(${d.x}, ${d.y})`)

      // 创建扇形
      currentArcGroup
        .selectAll('.arc')
        .data([0, 1, 2])
        // .enter()
        // .append('path')
        .join('path')
        // .attr('class', 'arc')
        .attr('class', (x, i) => `arc arc-${i}`)
        .attr('d', arc)
        .attr('fill', (x, i) => d3.schemeCategory10[i])
        .style('opacity', 1)

      // 添加icon
      currentArcGroup
        .selectAll('.arc-icon')
        .data([0, 1, 2])
        // .enter()
        // .append('image')
        .join('image')
        .attr('class', (d) => `arc-icon arc-icon-${d}`)
        .attr('xlink:href', (d, i) => {
          return i === 0 ? iconDe : i === 1 ? iconRe : iconDo
        })
        .attr('width', 20)
        .attr('height', 20)
        .attr('transform', (d, i) => {
          // 计算扇形中心点位置
          const angleStep = (2 * Math.PI) / 3 // 每个扇形120度
          const radius = 50 // 扇形的半径
          const angle = i * angleStep + angleStep / 2 // 取扇形中心角度
          const centerRadius = (radius + (radius - 20)) / 2 // 取内外半径的中点
          const x = Math.cos(angle) * centerRadius // 计算x坐标
          const y = Math.sin(angle) * centerRadius // 计算y坐标
          if (i === 0) {
            return `translate(${-x + 5},${0}) translate(-10,0)` // 平移到扇形中心并偏移使图标居中
          } else if (i === 1) {
            return `translate(${2},${y / 2 - 8}) translate(10,-10)` // 平移到扇形中心并偏移使图标居中
          } else {
            return `translate(${x + 4},${-y}) translate(-10,-10)` // 平移到扇形中心并偏移使图标居中
          }
        })
        .attr('x', (d, i) => {
          const angle = i * angleStep + angleStep / 2
          // 计算扇形区域中心点，使用radius - 20作为半径(与arc的innerRadius对应)
          const centerRadius = (radius + (radius - 20)) / 2 // 取内外半径的中点
          return Math.cos(angle) * centerRadius - 10 // 减去icon宽度的一半以居中
        })
        .attr('y', (d, i) => {
          const angle = i * angleStep + angleStep / 2
          const centerRadius = (radius + (radius - 20)) / 2 // 取内外半径的中点
          return Math.sin(angle) * centerRadius - 10 // 减去icon高度的一半以居中
        })
        .style('transition', 'transform 0.3s ease')
        .on('click', function (event, d) {
          // event.stopPropagation() // 阻止事件冒泡
          // console.log('d', d)
          if (d === 2) {
            // 刷题
            // console.log(selectedNode.value, '节点信息')
            const url = router.resolve({
              path: '/study/exam',
              query: {
                keyword_id: selectedNode.value.id,
                keyword_name: encodeURIComponent(selectedNode.value.name),
                course_id: route.query.cid,
                pid: route.query.pid,
              },
            })
            window.open(url.href, '_blank')
          } else if (d === 0) {
            // 查看信息
            drawer.value = true
            showDetail.value = selectedNode.value
          } else if (d === 1) {
            // 查看关系
            router.push({
              path: '/study/node',
              query: {
                node_id: selectedNode.value.id,
                course_id: route.query.cid,
                pid: route.query.pid,
              },
            })
          }
          d3.selectAll('.tooltip').remove()
        })
      // 为icon添加悬浮提示
      currentArcGroup
        .selectAll('.arc-icon')
        .on('mouseover', function (event, d) {
          // 定义每个icon对应的提示文本
          const tooltipText =
            d === 0
              ? '查看详情'
              : d === 1
              ? '查看关系'
              : d === 2
              ? '练习题目'
              : ''

          // 创建或更新提示框
          const tooltip = d3
            .select('body')
            .append('div')
            .attr('class', 'tooltip')
            .style('position', 'absolute')
            .style('background-color', 'rgba(0,0,0,0.7)')
            .style('color', 'white')
            .style('padding', '5px 10px')
            .style('border-radius', '4px')
            .style('font-size', '12px')
            .style('pointer-events', 'none')
            .style('opacity', 0)

          // 设置提示框位置和内容
          tooltip
            .html(tooltipText)
            .style('left', event.pageX + 10 + 'px')
            .style('top', event.pageY - 10 + 'px')
            .transition()
            .duration(200)
            .style('opacity', 1)
        })
        .on('mouseout', function () {
          // 移除提示框
          d3.selectAll('.tooltip').remove()
        })

      // 为扇形添加点击事件
      currentArcGroup.selectAll('.arc').on('click', function (event, d) {
        // console.log('d', d)
        if (d === 0) {
          // TODO: 刷题
          // console.log(selectedNode.value, '节点信息')
          const url = router.resolve({
            path: '/study/exam',
            query: {
              keyword_id: selectedNode.value.id,
              keyword_name: encodeURIComponent(selectedNode.value.name),
              course_id: route.query.cid,
              pid: route.query.pid,
            },
          })
          window.open(url.href, '_blank')
        } else if (d === 1) {
          // TODO: 查看信息
          drawer.value = true
          showDetail.value = selectedNode.value
          // console.log(selectedNode.value, '节点信息')
        } else if (d === 2) {
          // TODO: 查看关系
          router.push({
            path: '/study/node',
            query: {
              node_id: selectedNode.value.id,
              course_id: route.query.cid,
              pid: route.query.pid,
            },
          })
        }
        d3.selectAll('.tooltip').remove()
        // event.stopPropagation() // 阻止事件冒泡
      })
      // 为扇形添加悬浮提示
      currentArcGroup
        .selectAll('.arc')
        .on('mouseover', function (event, d) {
          // 定义每个扇形对应的提示文本
          const tooltipText =
            d === 0
              ? '练习题目'
              : d === 1
              ? '查看详情'
              : d === 2
              ? '查看关系'
              : ''

          // 创建或更新提示框
          const tooltip = d3
            .select('body')
            .append('div')
            .attr('class', 'tooltip')
            .style('position', 'absolute')
            .style('background-color', 'rgba(0,0,0,0.7)')
            .style('color', 'white')
            .style('padding', '5px 10px')
            .style('border-radius', '4px')
            .style('font-size', '12px')
            .style('pointer-events', 'none')
            .style('opacity', 0)

          // 设置提示框位置和内容
          tooltip
            .html(tooltipText)
            .style('left', event.pageX + 10 + 'px')
            .style('top', event.pageY - 10 + 'px')
            .transition()
            .duration(200)
            .style('opacity', 1)
        })
        .on('mouseout', function () {
          // 移除提示框
          d3.selectAll('.tooltip').remove()
        })
    }
  })
  // 添加缩放功能
  const zoom = d3
    .zoom()
    .scaleExtent([0.1, 4]) // 设置缩放范围
    .on('zoom', (event) => {
      g.attr('transform', event.transform)
    })

  // 将缩放行为应用到SVG容器
  svg.call(zoom).on('dblclick.zoom', null) // 禁用双击缩放
  // 点击非节点区域时隐藏所有大圆环
  g.on('click', function () {
    if (currentBigCircle) {
      currentBigCircle.remove()
      currentArcGroup.remove()
      currentBigCircle = null
      currentArcGroup = null
      selectedNode.value = null
    }
  })
}
const computedWidthHeight = () => {
  const rectDOM = svgContainer.value.getBoundingClientRect()
  width.value = rectDOM.width.toFixed(0)
  height.value = rectDOM.height.toFixed(0)
}

const getChartData = async () => {
  loading.value = true
  try {
    const res: any = await httpKnowledgeChart({
      course_id: route.query.cid,
      limit: 300,
      keyword: query.keyword,
    })
    if (res) {
      links.value = res.edges
      nodes.value = res.nodes.map((item) => {
        return {
          ...item,
          group: item.label,
        }
      })
    }
  } catch (error) {
  } finally {
    loading.value = false
  }
}
const handleChangeKeyword = async (value) => {
  await getChartData()
  d3.select('#graph').selectAll('svg').remove()
  renderChart()
  // 清除旧的SVG内容
}

onMounted(async () => {
  await getChartData()
  computedWidthHeight()
  window.addEventListener('resize', computedWidthHeight)
  renderChart()
})
onUnmounted(() => {
  window.removeEventListener('resize', computedWidthHeight)
  if (simulation.value) {
    simulation.value.stop()
  }
})
</script>
<style lang="scss">
.d3-wrapper {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  // background-color: #d3e7ee;
  background-color: #fafafa;
  .chart-header {
    border-bottom: 1px solid #ccc;
    padding: 4px 10px;
    .chart-header-title {
      display: flex;
      align-items: center;
      gap: 4px;
    }
  }
}
.graph-container {
  height: calc(100% - 40px);
  min-height: 600px;
  width: 100%;
  overflow: hidden;
  .graph-containers {
    width: calc(100% - 200px);
  }
}

.node {
  cursor: pointer;
}

.big-circle {
  fill: none;
  /* stroke: steelblue; */
  stroke-width: 0;
}

.small-circle {
  stroke: white;
  stroke-width: 2;
}

.arc {
  stroke: none;
  opacity: 0.7;
  transition: opacity 0.3s ease, fill 0.3s ease;
  cursor: pointer;
  position: relative;

  &:hover {
    fill: orange !important;
  }
}

.arc-text {
  font-size: 12px;
  /* text-anchor: middle; */
  alignment-baseline: middle;
  cursor: pointer;
}

.link {
  stroke: #aaa;
  stroke-width: 2;
}

.node-text {
  font-size: 12px;
  fill: #000;
  text-anchor: middle;
}

.relationship-text {
  font-size: 16px;
  fill: black;
  font-weight: bold;
  pointer-events: none; /* 防止文本框影响点击事件 */
}

.arc-label {
  font-size: 14px;
  fill: #000;
  /* pointer-events: none; */
  position: absolute;
}

.arc-icon {
  position: absolute;
  z-index: 999;
  cursor: pointer;
}

.arc-group:has(.arc-icon-0:hover) > .arc-1 {
  fill: orange !important;
}

.arc-group:has(.arc-icon-1:hover) > .arc-2 {
  fill: orange !important;
}

.arc-group:has(.arc-icon-2:hover) > .arc-0 {
  fill: orange !important;
}
</style>
